package connezione;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;


/**
 * 

 * Esta classe representa o pool de conex�es. Ela � respons�vel por instanciar
 * inicialmente algumas conex�es (definido o n�mero inicial pelo usu�rio) e 
 * acrescentar novas conex�es de acordo com a demanda.
 * 
 * 
 */
public class ConnectionPool {
	
	private int initialCapacity;
	private int maxCapacity;
	private int step;
	private String connectionString;
	private String driver;
	private Properties properties;
	private long timeOut;
	private List<PooledConnection> unavailable, available;
	
	private ConnectionPool(String driver) throws ClassNotFoundException{
		this.driver = driver;
		// 1) Carrega o driver para o banco de dados
		Class.forName(driver);
		this.unavailable = new ArrayList<PooledConnection>();
		this.available = new ArrayList<PooledConnection>();
	}
	
	private void init(){
		// inicializa��o do pool
		for(int i = 0; i < this.getInitialCapacity(); i++){
			PooledConnection conn = this.createConnection();
			this.available.add(conn);
		}
	}

	public void setInitialCapacity(int initialCapacity) {
		this.initialCapacity = initialCapacity;
	}

	public int getInitialCapacity() {
		return initialCapacity;
	}

	public void setMaxCapacity(int maxCapacity) {
		this.maxCapacity = maxCapacity;
	}

	public int getMaxCapacity() {
		return maxCapacity;
	}

	public void setStep(int step) {
		this.step = step;
	}

	public int getStep() {
		return step;
	}

	public void setConnectionString(String connectionString) {
		this.connectionString = connectionString;
	}

	public String getConnectionString() {
		return connectionString;
	}

	public void setDriver(String driver) {
		this.driver = driver;
	}

	public String getDriver() {
		return driver;
	}

	public void setProperties(Properties properties) {
		this.properties = properties;
	}

	public Properties getProperties() {
		return properties;
	}
	
	public synchronized void checkIn(PooledConnection conn){
		PooledConnection pconn = (PooledConnection)conn;		
		this.available.add(conn);
		this.unavailable.remove(conn);
		
		System.out.println(" Devolvendo " + pconn + "  ****** idade " + pconn.getAge());
		// notifica todas as threads que est�o esperando para
		// continuarem suas opera��es.
		
		this.notifyAll();
	}
	
	public synchronized PooledConnection checkOut(){
		if (this.available.size()>0){	
			for(PooledConnection conn: this.available){
				// conex�o v�lida
				if(this.isValid(conn)){
					this.available.remove(conn);
					this.unavailable.add(conn);
					System.out.println("disponibilizou conex�o: " + conn);
					return conn;
				} else {
					this.expire(conn);
					continue;
				}	
			}
		} else {
			// Verificar se pode-se criar novas conex�es.
			if(this.creatorManager()){
				// se criou novas conex�es chame o m�todo recursivamente.
				return this.checkOut();
			} else {
				// se n�o criou novas conex�es fa�a a thread esperar a devolu��o
				// de uma conex�o.
				try {
					System.out.println("Thread vai esperar " + Thread.currentThread().getId());
					wait();
					// Quando acordar, chamada recursiva para o m�todo.
					return this.checkOut();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		return null;
	}
	
	public int totalConnections(){
		return this.available.size() + this.unavailable.size();
	}
	public int availableConnections(){
		return this.available.size();
	}
	public int unavailableConnections(){
		return this.unavailable.size();
	}
	
	private boolean isValid(Connection conn){
		// Verifica se n�o deu timeout
		PooledConnection pconn = (PooledConnection)conn;
		long connTime = pconn.getCreationTime();
		long now = System.currentTimeMillis();
		if(this.timeOut > 0 && (now - connTime) > this.timeOut) {
			return false;
		}
		
		// Verifica se est� aberta
		try {
			if (conn.isClosed()) return false;
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return true;
	}
	
	private void expire(PooledConnection conn){
		PooledConnection pconn = conn;
		try {
			this.available.remove(conn);
			this.unavailable.remove(conn);
			if (!pconn.isClosed()) pconn.reallyClose();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	private PooledConnection createConnection(){
		try {	
			// 2) Abre a conex�o
			Connection conn =  DriverManager.getConnection(this.connectionString, this.properties);
			PooledConnection pconn = new PooledConnection(conn, this);
			return pconn;
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	private boolean creatorManager(){
		if(this.availableConnections() == 0 && this.totalConnections() < this.getMaxCapacity()){
			for(int i = 0; i < this.getStep(); i++){
				PooledConnection conn = this.createConnection();
				this.available.add(conn);
				System.out.println("CRIOU connection " + conn);
			}
			return true;
		}
		return false;
	}
	
	public void setTimeOut(long miliSeconds) {
		this.timeOut = miliSeconds;
	}

	public long getTimeOut() {
		return timeOut;
	}
	
	public synchronized void releaseAll(){
		//TODO release all connections
		Iterator<PooledConnection> it = this.available.iterator();
		while(it.hasNext()){
			PooledConnection conn = it.next();
			it.remove();
			PooledConnection pconn = conn;
			try {
				pconn.reallyClose();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		
		
		it = this.unavailable.iterator();
		while(it.hasNext()){
			PooledConnection conn = it.next();
			it.remove();
			PooledConnection pconn = conn;
			try {
				pconn.reallyClose();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}

	public static class Builder {
		private int initialCapacity;
		private int maxCapacity;
		private int step;
		private String connectionString;
		private String driver;
		private long timeOut;
		private Properties properties;
		
				
		public Builder setInitialCapacity(int initCapacity){
			this.initialCapacity = initCapacity;
			return this;
		}
		public Builder setMaxCapacity(int maxCapacity){
			this.maxCapacity = maxCapacity;
			return this;
		}
		public Builder setStep(int step){
			this.step = step;
			return this;
		}
		public Builder setConnectionString(String cstr){
			this.connectionString = cstr;
			return this;
		}
		public Builder setDriver(String driver){
			this.driver = driver;
			return this;
		}
		public Builder setProperties(Properties prop){
			this.properties = prop;
			return this;
		}
		
		public Builder setTimeOut(long miliSeconds){
			this.timeOut = miliSeconds;
			return this;
		}
		
		public ConnectionPool build() throws ClassNotFoundException{
			ConnectionPool pool = null;
			try {
				pool = new ConnectionPool(this.driver);
				pool.setInitialCapacity(this.initialCapacity);
				pool.setMaxCapacity(this.maxCapacity);
				pool.setStep(this.step);
				pool.setConnectionString(this.connectionString);
				pool.setProperties(this.properties);
				pool.setTimeOut(this.timeOut);
					
				pool.init();
			} catch (ClassNotFoundException e) {
				throw new ClassNotFoundException("Informe pelo menos o driver ");
			}
			
			return pool;
		}
	}
}
